Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 4389b643d9e4e528a9c0b3a63d3c26c07dcbd192


Parents : 30d95f7
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-25T16:28:44-05:00

feat(android): update meshchat_wrapper with server loop control and improve error handling for Android notification bridge

Changes

2 files changed, 82 insertions(+), 2 deletions(-)


Diff

diff --git a/android/app/src/main/python/meshchat_wrapper.py b/android/app/src/main/python/meshchat_wrapper.py
index 91813e2e..488d4aec 100644
--- a/android/app/src/main/python/meshchat_wrapper.py
+++ b/android/app/src/main/python/meshchat_wrapper.py
@@ -3,6 +3,11 @@
import os
import signal
import sys
+import threading
+
+# Prevents a second meshchat main() if Java starts two threads (e.g. activity edge cases).
+_server_loop_lock = threading.Lock()
+_server_loop_active = False
def _ensure_android_reticulum_config(reticulum_config_dir):
@@ -67,6 +72,12 @@ def _patch_aiohttp_run_app_for_android():
def start_server(port=8000, app_files_dir=None):
+ global _server_loop_active
+ with _server_loop_lock:
+ if _server_loop_active:
+ print("meshchat_wrapper: start_server ignored (server loop already active)")
+ return
+ _server_loop_active = True
try:
storage_dir = None
reticulum_config_dir = None
@@ -91,10 +102,14 @@ def start_server(port=8000, app_files_dir=None):
signal.signal = _safe_signal
asyncio_signal_patch = _patch_asyncio_signal_handlers_for_android()
aiohttp_run_app_patch = _patch_aiohttp_run_app_for_android()
- from meshchatx.android_push_bridge import install_websocket_hook
from meshchatx.meshchat import ReticulumMeshChat, main
- install_websocket_hook(ReticulumMeshChat)
+ try:
+ from meshchatx.android_push_bridge import install_websocket_hook
+
+ install_websocket_hook(ReticulumMeshChat)
+ except Exception as hook_exc:
+ print(f"meshchat_wrapper: install_websocket_hook skipped: {hook_exc}")
sys.argv = [
"meshchat",
@@ -125,3 +140,6 @@ def start_server(port=8000, app_files_dir=None):
traceback.print_exc()
raise
+ finally:
+ with _server_loop_lock:
+ _server_loop_active = False

diff --git a/meshchatx/android_push_bridge.py b/meshchatx/android_push_bridge.py
index 6aa190b8..0291f14d 100644
--- a/meshchatx/android_push_bridge.py
+++ b/meshchatx/android_push_bridge.py
@@ -73,6 +73,42 @@ def _notify_java(title: str, body: str, dedupe_hex: str | None) -> None:
logger.debug("showInboundMessage failed: %s", exc)
+def _notify_incoming_call_java(caller_name: str, dedupe_hex: str | None) -> None:
+ try:
+ from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped]
+ except Exception as exc:
+ logger.debug("Android notification bridge unavailable: %s", exc)
+ return
+ try:
+ AndroidNotificationBridge.showIncomingCall(caller_name, dedupe_hex)
+ except Exception as exc:
+ logger.debug("showIncomingCall failed: %s", exc)
+
+
+def _notify_missed_call_java(title: str, body: str, dedupe_hex: str | None) -> None:
+ try:
+ from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped]
+ except Exception as exc:
+ logger.debug("Android notification bridge unavailable: %s", exc)
+ return
+ try:
+ AndroidNotificationBridge.showMissedCall(title, body, dedupe_hex)
+ except Exception as exc:
+ logger.debug("showMissedCall failed: %s", exc)
+
+
+def _cancel_incoming_call_notification_java() -> None:
+ try:
+ from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped]
+ except Exception as exc:
+ logger.debug("Android notification bridge unavailable: %s", exc)
+ return
+ try:
+ AndroidNotificationBridge.cancelIncomingCallNotification()
+ except Exception as exc:
+ logger.debug("cancelIncomingCallNotification failed: %s", exc)
+
+
def _after_websocket_broadcast(data: object) -> None:
if not isinstance(data, str):
return
@@ -82,6 +118,32 @@ def _after_websocket_broadcast(data: object) -> None:
return
if not isinstance(payload, dict):
return
+ t = payload.get("type")
+ if t in ("telephone_call_ended", "telephone_call_established"):
+ _cancel_incoming_call_notification_java()
+ return
+ if t == "telephone_ringing":
+ ch = payload.get("remote_identity_hash")
+ name = (payload.get("remote_identity_name") or "").strip() or "Mesh"
+ ded = ch if isinstance(ch, str) and len(ch) >= 8 else None
+ _notify_incoming_call_java(name, ded)
+ return
+ if t == "telephone_missed_call":
+ sender = (payload.get("remote_identity_name") or "").strip() or "Mesh"
+ ch = payload.get("remote_identity_hash")
+ h = ch if isinstance(ch, str) and len(ch) >= 8 else None
+ if sender and sender != "Mesh":
+ title = "Missed call"
+ body = f"Missed call from {sender}"
+ elif isinstance(ch, str) and ch:
+ short_h = f"{ch[:6]}" if len(ch) > 6 else ch
+ title = "Missed call"
+ body = f"From {short_h}..."
+ else:
+ title = "Missed call"
+ body = "Missed call"
+ _notify_missed_call_java(title, body, h)
+ return
pair = lxmf_delivery_notification_text(payload)
if not pair:
return


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────